/*
Copyright 2021 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package crd2armor

import (
	"bytes"
	"errors"
	"slices"
	"text/template"

	apparmorprofileapi "sigs.k8s.io/security-profiles-operator/api/apparmorprofile/v1alpha1"
)

var appArmorTemplate = `
# Generated by https://github.com/kubernetes-sigs/security-profiles-operator, do not edit by hand.
#include <tunables/global>
profile {{.Name}} flags=({{.ProfileMode}},attach_disconnected,mediate_deleted) {
  #include <abstractions/base>

  # Executable rules
{{ if ne .Abstract.Executable nil }}{{ if ne .Abstract.Executable.AllowedExecutables nil }}
{{range $allowed := .Abstract.Executable.AllowedExecutables}}  {{$allowed}} ixr,
{{end}}{{end}}
{{ if ne .Abstract.Executable.AllowedLibraries nil }}
{{range $allowedlib := .Abstract.Executable.AllowedLibraries}}  {{$allowedlib}} mr,
{{end}}{{end}}{{end}}
  {{ if .AllowMount }}
  /sbin/fsck ixr,
  /sbin/fsck.ext4 ixr,
  {{end}}

  # Filesystem rules
{{ if ne .Abstract.Filesystem nil }}{{ if ne .Abstract.Filesystem.ReadOnlyPaths nil }}
{{range $readonly := .Abstract.Filesystem.ReadOnlyPaths}}  {{$readonly}} r,
{{end}}
{{range $readonly := .Abstract.Filesystem.ReadOnlyPaths}}  deny {{$readonly}} wlk,
{{end}}{{end}}
{{ if ne .Abstract.Filesystem.WriteOnlyPaths nil }}
{{range $writeonly := .Abstract.Filesystem.WriteOnlyPaths}}  {{$writeonly}} wlk,
{{end}}
{{range $writeonly := .Abstract.Filesystem.WriteOnlyPaths}}  deny {{$writeonly}} r,
{{end}}{{end}}
{{ if ne .Abstract.Filesystem.ReadWritePaths nil }}
{{range $readwrite := .Abstract.Filesystem.ReadWritePaths}}  {{$readwrite}} rwlk,
{{end}}{{end}}{{end}}

  # Network rules
{{ if ne .Abstract.Network nil }}{{ if ne .Abstract.Network.AllowRaw nil }}
{{ if .Abstract.Network.AllowRaw}}  network raw,{{else}}  deny network raw,
{{end}}{{end}}
{{ if ne .Abstract.Network.Protocols nil }}
{{if ne .Abstract.Network.Protocols.AllowTCP nil }}
{{if .Abstract.Network.Protocols.AllowTCP}}  network tcp,
{{end}}{{end}}{{if ne .Abstract.Network.Protocols.AllowUDP nil }}
{{if .Abstract.Network.Protocols.AllowUDP}}  network udp,
{{end}}{{end}}{{end}}{{end}}

  # Capabilities rules
{{ if ne .Abstract.Capability nil}}{{range $cap := .Abstract.Capability.AllowedCapabilities}}  capability {{$cap}},
{{end}}{{end}}

  {{ if .AllowMount }}
  mount,
  remount,
  umount,
  {{end}}

  # Raw rules placeholder

  # Add default deny for known information leak/priv esc paths
  deny @{PROC}/* w,   # deny write for all files directly in /proc (not in a subdir)
  deny @{PROC}/{[^1-9],[^1-9][^0-9],[^1-9s][^0-9y][^0-9s],[^1-9][^0-9][^0-9][^0-9]*}/** w,
  deny @{PROC}/sys/[^k]** w,  # deny /proc/sys except /proc/sys/k* (effectively /proc/sys/kernel)
  deny @{PROC}/sys/kernel/{?,??,[^s][^h][^m]**} w,  # deny everything except shm* in /proc/sys/kernel/
  deny @{PROC}/sysrq-trigger rwklx,
  deny @{PROC}/mem rwklx,
  deny @{PROC}/kmem rwklx,
  deny @{PROC}/kcore rwklx,
  deny /sys/firmware/efi/efivars/** rwklx,
}
`

type apparmorTemplateArgs struct {
	Name        string
	ProfileMode string
	Abstract    *apparmorprofileapi.AppArmorAbstract
	AllowMount  bool
}

// GenerateProfile uses the CRD representation of an abstracted profile to generate a
// full AppArmor profile.
func GenerateProfile(name string, complainMode bool, abstract *apparmorprofileapi.AppArmorAbstract) (string, error) {
	var generated bytes.Buffer

	// This is a bit of a hack to allow containers that exercise raw I/O to use `mount`.
	// Eventually we should get rid of this and record mount/umount properly.
	allowMount := (abstract != nil &&
		abstract.Capability != nil &&
		abstract.Capability.AllowedCapabilities != nil &&
		slices.Contains(abstract.Capability.AllowedCapabilities, "sys_rawio"))

	templateArgs := apparmorTemplateArgs{
		Name:        name,
		ProfileMode: profileMode(complainMode),
		Abstract:    abstract,
		AllowMount:  allowMount,
	}

	if abstract == nil {
		return "", errors.New("abstract cannot be nil")
	}

	tpl, err := template.New("apparmor").Parse(appArmorTemplate)
	if err != nil {
		return "", err
	}

	if err := tpl.Execute(&generated, templateArgs); err != nil {
		return "", err
	}

	return generated.String(), nil
}

func profileMode(complainMode bool) string {
	if complainMode {
		return "complain"
	}

	return "enforce"
}
